package test

import test.Prop.{FailedCase, SuccessCount}

import scala.util.Left

/**
 * User: bigbully
 * Date: 15/3/1
 * Time: 上午9:56
 */
object MyTesting extends App{


}
trait Status

case object Proven extends Status
case object Unfalsified extends Status

import Prop._
case class Prop(run:(TestCases, RNG) =>Result) {

}
object Prop {
  import Gen._
  type TestCases = Int
  type Result = Either[FailedCase, (Status, SuccessCount)]
  type SuccessCount = Int
  type FailedCase = String

  def forAll[A](a:Gen[A])(f:A =>Boolean):Prop = Prop {
    (n,rng) => {
      def go(i: Int, j: Int, s: Stream[Option[A]], onEnd: Int => Result): Result =
        if (i == j) Right((Unfalsified, i))
        else s match {
          case Cons(h,t) => h() match {
            case Some(h) =>
              try {
                if (f(h)) go(i+1,j,t(),onEnd)
                else Left(h.toString) }
              catch { case e: Exception => Left(buildMsg(h, e)) }
            case None => Right((Unfalsified,i))
          }
          case _ => onEnd(i)
        }
      go(0, n/3, a.exhaustive, i => Right((Proven, i))) match {
        case Right((Unfalsified,_)) =>
          val rands = randomStream(a)(rng).map(Some(_))
          go(n/3, n, rands, i => Right((Unfalsified, i)))
        case s => s // If proven or failed, stop immediately
      }
    }
  }

  def buildMsg[A](s: A, e: Exception): String =
    "test case: " + s + "\n" +
      "generated an exception: " + e.getMessage + "\n" +
      "stack trace:\n" + e.getStackTrace.mkString("\n")
}
case class Gen[+A](sample: State[RNG, A], exhaustive:Stream[Option[A]]) {
  import Gen._
  def map[B](f: A => B): Gen[B] = {
    Gen(sample.map(f), exhaustive.map(_.map(f)))
  }

  def map2[B,C](g: Gen[B])(f: (A,B) => C): Gen[C] = {
    Gen(sample.map2(g.sample)(f), map2Stream(exhaustive, g.exhaustive)(map2Option(_, _)(f)))
  }

  def flatMap[B](f: A => Gen[B]): Gen[B] = {
    Gen(sample.flatMap(f(_).sample), exhaustive.flatMap{
      case Some(a) => f(a).exhaustive
      case None => unbounded
    })
  }
}


object Gen {

  type Domain[+A] = Stream[Option[A]]

  def bounded[A](a:Stream[A]):Domain[A] = a map (Some(_))
  def unbounded: Domain[Nothing] = Stream(None)

  def unit[A](a: => A) :Gen[A] = {
    Gen(State.unit(a), bounded(Stream(a)))
  }

  def boolean:Gen[Boolean] = {
    Gen(State(RNG.boolean), bounded(Stream(true, false)))
  }


  def listOf[A](n:Int, a:Gen[A]):Gen[List[A]] = {
    Gen(State.sequence(List.fill(n)(a.sample)),
      cartesian(Stream.constant(a.exhaustive).take(n)).
        map(l => sequenceOption(l.toList)))
  }

  def cartesian[A](s: Stream[Stream[A]]): Stream[Stream[A]] =
    s.foldRight(Stream(Stream[A]()))((hs,ts) => map2Stream(hs,ts)(Stream.cons(_,_)))

  def map2Option[A,B,C](oa: Option[A], ob: Option[B])(f: (A,B) => C): Option[C] =
    for { a <- oa; b <- ob } yield f(a,b)

  def map2Stream[A,B,C](sa: Stream[A], sb: => Stream[B])(f: (A,=>B) => C): Stream[C] =
    for { a <- sa; b <- sb } yield f(a,b)

  def sequenceOption[A](o: List[Option[A]]): Option[List[A]] =
    o.foldLeft[Option[List[A]]](Some(List()))(
      (t,h) => map2Option(h,t)(_ :: _)).map(_.reverse)

  def forAll[A](a:Gen[A])(f:A => Boolean) :Prop = ???

  def choose(start:Int, stopExclusive:Int) :Gen[Int] = {
    Gen(State(RNG.nonNegativeInt).map(n => start + n % (stopExclusive-start)), Stream.from(start).take(stopExclusive))
  }

  def uniform: Gen[Double] = {
    Gen(State(RNG.double), unbounded)
  }

  def choose(i: Double, j: Double): Gen[Double] = {
    Gen(State(RNG.double).map(d => i + d*(j-i)), unbounded)
  }

  def even(start: Int, stopExclusive: Int): Gen[Int] =
    choose(start, if (stopExclusive%2 == 0) stopExclusive - 1 else stopExclusive).
      map (n => if (n%2 != 0) n+1 else n)

  def odd(start: Int, stopExclusive: Int): Gen[Int] =
    choose(start, if (stopExclusive%2 != 0) stopExclusive - 1 else stopExclusive).
      map (n => if (n%2 == 0) n+1 else n)

  def sameParity(from: Int, to: Int): Gen[(Int,Int)] = for {
    i <- choose(from,to)
    j <- if (i%2 == 0) even(from,to) else odd(from,to)
  } yield (i,j)

  def listOfN_1[A](n: Int, g: Gen[A]): Gen[List[A]] = {
    List.fill(n)(g).foldRight(unit(List[A]()))((a,b) => a.map2(b)(_ :: _))
  }

  def union_1[A](g1: Gen[A], g2: Gen[A]): Gen[A] =
    boolean.flatMap(b => if (b) g1 else g2)

  def union[A](g1: Gen[A], g2: Gen[A]): Gen[A] =
    Gen(
      State(RNG.boolean).flatMap(b => if (b) g1.sample else g2.sample),
      interleave(g1.exhaustive, g2.exhaustive)
    )

  def interleave[A](s1: Stream[A], s2: Stream[A]): Stream[A] =
    s1.zipAll(s2).flatMap { case (a,a2) => Stream((a.toList ++ a2.toList): _*) }

  def weighted[A](g1: (Gen[A],Double), g2: (Gen[A],Double)): Gen[A] = {
    /* The probability we should pull from `g1`. */
    val g1Threshold = g1._2.abs / (g1._2.abs + g2._2.abs)

    /* Some random booleans to use for selecting between g1 and g2 in the exhaustive case.
     * Making up a seed locally is fine here, since we just want a deterministic schedule
     * with the right distribution. */
    def bools: Stream[Boolean] =
      randomStream(uniform.map(_ < g1Threshold))(RNG.Simple(302837L))

    Gen(State(RNG.double).flatMap(d => if (d < g1Threshold) g1._1.sample else g2._1.sample),
      interleave(bools, g1._1.exhaustive, g2._1.exhaustive))
  }

  def randomStream[A](g: Gen[A])(rng: RNG): Stream[A] =
    Stream.unfold(rng)(rng => Some(g.sample.run(rng)))

}
